<template>
  <div class="resizable-tree-wrapper" v-if="treeData.length">
    <div class="tree-and-content">
      <div class="tree-pane">
        <!-- 移除顶部局部加载按钮，避免跳动 -->
        <el-tree
          :data="treeData"
          node-key="key"
          :props="treeProps"
          default-expand-all
          highlight-current
          :expand-on-click-node="false"
          class="project-tree"
          @node-click="handleNodeClick"
        >
          <template #default="{ node, data }">
            <div class="node-row" :class="{ editing: editingNode === data }">
              <div class="node-left">
                <el-icon v-if="data.children && data.children.length" class="folder-icon">
                  <!-- 改为明确的条件渲染，避免 <component :is> 类型推断问题 -->
                  <folder-opened v-if="node.expanded" />
                  <folder v-else />
                </el-icon>
                <el-icon v-else class="file-icon">
                  <document />
                </el-icon>
                <svg-icon v-if="data.icon" :icon-class="data.icon" class="custom-icon" />
                <!-- 双击改名增加权限判断：只有 canRename(data) 为 true 才允许触发 startRename -->
                <span v-if="editingNode !== data" class="label" @dblclick.stop="canRename(data) && startRename(data)">{{ data.name }}</span>
                <el-input
                  v-else
                  ref="renameInputRef"
                  v-model="editName"
                  size="small"
                  class="rename-input"
                  @keyup.enter="confirmRename"
                  @blur="confirmRename"
                />
              </div>
              <div v-if="hasActions(data)" class="node-actions">
                <el-dropdown>
                  <span class="actions-trigger" @click.stop>
                    <el-button size="small" text>
                      操作
                      <el-icon class="more-icon"><MoreFilled /></el-icon>
                    </el-button>
                  </span>
                  <template #dropdown>
                    <el-dropdown-menu>
                      <el-dropdown-item v-if="data.uploadFile" @click.prevent.stop="uploadFileForNode(data)">上传</el-dropdown-item>
                      <!-- <el-dropdown-item v-if="canRename(data)" @click.prevent.stop="startRename(data)">改名</el-dropdown-item> -->
                      <el-dropdown-item v-if="data.removal" @click.prevent.stop="confirmRemoveNode(data)" class="danger-item">删除</el-dropdown-item>
                      <el-dropdown-item v-if="data.addChildren" @click.prevent.stop="addChildNode(data)">新增</el-dropdown-item>
                    </el-dropdown-menu>
                  </template>
                </el-dropdown>
              </div>
            </div>
          </template>
        </el-tree>
      </div>
      <div v-if="currentNode && ActiveComponent" class="content-pane">
        <component
          :is="ActiveComponent"
          :key="currentNode.key + ':' + (currentNode.type||'') + ':' + renderVersion"
          :node="currentNode"
          :project-id="projectId"
          :geoFileUrl="geoFileUrl"
          @cancel="handlePanelCancel"
          @confirm="handlePanelConfirm"
        />
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
/**
 * resizableTree/index.vue 组件说明
 * ------------------------------------------------------------
 * 展示项目树（Pinia 中的纯数据）并支持选择、改名、上传、动态加载对应组件；
 * 通过 Pinia 订阅深度监听改动，差异快照去抖自动保存。
 */
import { ref, watch, onMounted, nextTick, computed, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { uploadProjectFile } from '@/api/system/file'
import { Document, Folder, FolderOpened, MoreFilled } from '@element-plus/icons-vue'
import { useTreeStore, type ProjectTreeNode } from '@/store/modules/tree'
import eventBus from '@/utils/eventBus'

// el-tree 的 props 映射
const treeProps = { label: 'name', children: 'children' }
const { projectId } = defineProps<{ projectId: string | null }>()

// Store & 计算属性
const treeStore = useTreeStore()
const treeData = computed<ProjectTreeNode[]>(() => treeStore.getTree(projectId))
const loading = computed<boolean>(() => treeStore.isLoading(projectId))

// 状态：改名
const editingNode = ref<ProjectTreeNode | null>(null)
const editName = ref('')
const renameInputRef = ref<any>()

// 当前节点与动态组件
const currentNode = ref<ProjectTreeNode | null>(null)
const ActiveComponent = ref<any>(null)
const componentCache: Record<string, any> = {}

// renderVersion ref 定义
const renderVersion = ref(0)
const closedNodes = ref<Set<string>>(new Set())
let unsubscribe: (() => void) | null = null

// partChildren 模型文件 url
const geoFileUrl = ref<string | null>(null)
function handlePartChildrenSelected(payload: { filePath?: string }) {
  geoFileUrl.value = payload.filePath || null
}

/**
 * 预扫描取得所有可按需加载的组件：
 *  - 目录形式： ./components/<type>/index.vue
 *  - 单文件形式：./components/<type>.vue
 * import.meta.glob 返回的是一个对象，key 为路径，value 为懒加载函数
 */
const componentModules = import.meta.glob('./components/*/index.vue')
const standaloneModules = import.meta.glob('./components/*.vue')

/** 判断节点是否允许改名 */
function canRename(node: ProjectTreeNode): boolean {
  return node.rename && node.submenu
}

/** 进入改名模式：设置当前编辑节点，并在 nextTick 中聚焦输入框 */
function startRename(node: ProjectTreeNode) {
  if (!canRename(node)) return
  editingNode.value = node
  editName.value = node.name
  nextTick(() => {
    const inputEl = (renameInputRef.value?.input as HTMLInputElement | undefined)
    inputEl?.focus()
  })
}

/** 确认改名：去除前后空格，非空则提交到 store */
function confirmRename() {
  if (editingNode.value && canRename(editingNode.value)) {
    const trimmed = editName.value.trim()
    if (trimmed) {
      treeStore.renameNode(editingNode.value, trimmed)
      // 局部调用移除，统一由订阅触发保存
    }
  }
  editingNode.value = null
  editName.value = ''
}

/** 节点是否显示操作（上传 / 改名 / 新增等）按钮区 */
function hasActions(n: ProjectTreeNode): boolean {
  if (!n.submenu) return false
  return n.uploadFile || n.rename || n.removal || n.addChildren
}

/** 新增子节点：读取对应模板 <type>Children.json，插入树中并可选进入改名 */
async function addChildNode(node: ProjectTreeNode) {
  const newNodes = await treeStore.addChildNodes(projectId, node)
  if (newNodes.length === 0) {
    if (!node.type) ElMessage.warning('该节点缺少 type，无法匹配模板')
    else ElMessage.warning(`未找到模板: ${node.type}Children.json`)
    return
  }
  if (newNodes.length && canRename(newNodes[0])) startRename(newNodes[0])
}

/** 上传文件：调用后端接口，成功后回写 filePath 与 fileId */
async function uploadFileForNode(node: ProjectTreeNode) {
  const input = document.createElement('input'); input.type = 'file'
  input.onchange = async (e: Event) => {
    const file = (e.target as HTMLInputElement).files?.[0]
    if (!file) return
    try {
      const resp: any = await uploadProjectFile(projectId, file)
      if (resp?.code === 200 && resp?.data?.filePath) {
        node.filePath = resp.data.filePath
        node.fileId = resp.data.id
        ElMessage.success('上传成功')
        if (node.type === 'partChildren' && node.filePath) {
          eventBus.emit('partChildren-file-selected', { filePath: node.filePath })
        }
      } else ElMessage.error(resp?.msg || '上传失败')
    } catch (err: any) { ElMessage.error(err?.message || '上传异常') }
  }
  input.click()
}

/**
 * 节点点击处理：
 * 1. 记录当前节点
 * 2. 按节点类型尝试加载右侧动态组件
 * 3. 如果节点类型为 'partChildren' 且有 filePath，派发全局自定义事件：
 *    window.dispatchEvent(new CustomEvent('partChildren-file-selected', { detail: { filePath } }))
 *    ThreeScene.vue 监听后可直接使用 detail.filePath 加载模型。
 */
function handleNodeClick(nodeData: ProjectTreeNode) {
  const same = currentNode.value && currentNode.value.key === nodeData.key
  currentNode.value = nodeData
  // 仅当再次点击同一个且之前被关闭时才强制重载
  if (same && closedNodes.value.has(nodeData.key)) {
    renderVersion.value++
    closedNodes.value.delete(nodeData.key)
  }
  loadTypeComponent(nodeData.type)
  if (nodeData.type === 'partChildren' && nodeData.filePath) {
    eventBus.emit('partChildren-file-selected', { filePath: nodeData.filePath })
  }
}

const emit = defineEmits<{ (e: 'component-loading'): void; (e: 'component-ready'): void; (e: 'tree-loading'): void; (e: 'tree-ready'): void }>()
// 生命周期：mounted 时初次加载树；projectId 变化时重新加载
onMounted(() => {
  if (projectId != null) treeStore.load(projectId)
  eventBus.on('partChildren-file-selected', handlePartChildrenSelected)
  unsubscribe = treeStore.$subscribe((mutation) => {
    if (projectId == null) return
    if (mutation.storeId === 'tree') scheduleAutoSave('pinia-subscribe')
  }, { deep: true })
})
onBeforeUnmount(() => {
  eventBus.off('partChildren-file-selected', handlePartChildrenSelected)
  unsubscribe?.()
})

watch(() => projectId, (val) => { if (val != null) treeStore.load(val) })

// 自动选中第一项并加载其组件（仅在未选中且数据初次到达时）
watch(treeData, (arr) => {
  if (!currentNode.value && arr.length) {
    currentNode.value = arr[0]
    loadTypeComponent(arr[0].type)
  }
})

// 动态加载组件
let token = 0
/**
 * 加载与节点 type 对应的右侧面板组件（在 handleNodeClick 与初始 treeData watch 中调用）
 */
function loadTypeComponent(type: string | null | undefined) {
  ActiveComponent.value = null
  token++
  if (!type || !/^[a-zA-Z0-9_-]+$/.test(type)) { emit('component-ready'); return }
  const cached = componentCache[type]
  if (cached) { ActiveComponent.value = cached; emit('component-ready'); return }
  const folderPath = `./components/${type}/index.vue`
  const filePath = `./components/${type}.vue`
  let rawLoader = (componentModules as Record<string, () => Promise<any>>)[folderPath]
  if (!rawLoader) rawLoader = (standaloneModules as Record<string, () => Promise<any>>)[filePath]
  if (!rawLoader) { emit('component-ready'); return }
  emit('component-loading')
  const currentToken = token
  rawLoader().then(mod => {
    if (currentToken !== token) return
    const comp = mod.default || mod
    componentCache[type] = comp
    ActiveComponent.value = comp
  }).catch(err => { if (currentToken === token) ElMessage.error('组件加载失败: ' + (err?.message || err)) })
    .finally(() => { if (currentToken === token) emit('component-ready') })
}

// ========== Auto-save configuration (renamed for clarity) ==========
const autoSaveDebounceMs = 800
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null
let lastPersistedSnapshot: string | null = null
let lastSaveErrorMsg: string | null = null
let lastSaveErrorTime = 0
const errorToastThrottleMs = 2000

// Snapshot helpers
function getCurrentSnapshot(): string {
  try { return JSON.stringify(treeStore.getTree(projectId)) } catch { return '' }
}
function hasSnapshotChanged(): boolean {
  const snap = getCurrentSnapshot()
  return snap !== lastPersistedSnapshot
}
function updatePersistedSnapshot() { lastPersistedSnapshot = getCurrentSnapshot() }

// Unified error toast (throttled for same message)
function showSaveError(msg: string) {
  const now = Date.now()
  if (lastSaveErrorMsg === msg && now - lastSaveErrorTime < errorToastThrottleMs) return
  ElMessage.error(msg)
  lastSaveErrorMsg = msg
  lastSaveErrorTime = now
}

// Perform actual save
async function performAutoSave(reason: string) {
  if (projectId == null) return
  if (!hasSnapshotChanged()) return
  try {
    const resp: any = await treeStore.saveTree(projectId as string)
    if (resp?.code === 200) {
      updatePersistedSnapshot()
      lastSaveErrorMsg = null
    } else {
      showSaveError(resp?.msg || '保存失败')
      console.warn('[AutoSave] non-200', reason, resp)
    }
  } catch (e: any) {
    showSaveError(e?.message || '保存异常')
    console.error('自动保存异常:', reason, e)
  }
}

function scheduleAutoSave(reason: string) {
  if (projectId == null || loading.value) return
  if (!hasSnapshotChanged()) return
  if (autoSaveTimer) clearTimeout(autoSaveTimer)
  autoSaveTimer = setTimeout(() => performAutoSave(reason), autoSaveDebounceMs)
}

// 暴露 geoFileUrl
defineExpose({ geoFileUrl })

function confirmRemoveNode(node: ProjectTreeNode) {
  if (!node.removal) return
  if (!projectId) { ElMessage.warning('缺少项目ID，无法删除'); return }
  const ok = treeStore.removeNode(projectId, node)
  if (ok) {
    closedNodes.value.delete(node.key)
    ElMessage.success('已删除')
    // 如果删除的是当前节点，清空右侧面板并尝试选中新的节点
    if (currentNode.value === node) {
      currentNode.value = null
      ActiveComponent.value = null
      const list = treeStore.getTree(projectId)
      if (list.length) {
        currentNode.value = list[0]
        loadTypeComponent(list[0].type)
      }
    }
    // 自动保存由订阅处理
  } else {
    ElMessage.error('删除失败')
  }
}

function handlePanelCancel() {
  if (currentNode.value) closedNodes.value.add(currentNode.value.key)
}
function handlePanelConfirm() {
  if (currentNode.value) closedNodes.value.delete(currentNode.value.key)
}
</script>
<style scoped lang="scss">
.resizable-tree-wrapper {
  width: 100%;
  height: 100%;
  background: var(--el-fill-color-blank);
  border: 1px solid var(--el-border-color);
  border-radius: 12px;
  box-shadow: 0 2px 6px rgba(0,0,0,.12), 0 4px 16px rgba(0,0,0,.08);
  padding: 8px 10px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  overflow: hidden; /* 保证内部溢出不破坏圆角 */
  transition: box-shadow .25s, border-color .25s, background .25s;
  position: relative; /* For pseudo-element positioning */
}
.tree-and-content { display:flex; flex:1; min-height:0; }
.tree-pane { width:260px; display:flex; flex-direction:column; min-height:0; }
.content-pane { flex:1; margin-left:12px; /* 移除红色边框 */ border:none; border-radius:8px; padding:8px; box-sizing:border-box; overflow:auto; position:relative; }
.resizable-tree-wrapper:hover {
  box-shadow: 0 6px 18px rgba(0,0,0,.18), 0 8px 28px rgba(0,0,0,.12);
}
html.dark .resizable-tree-wrapper {
  background: rgba(255,255,255,.06);
  border-color: rgba(255,255,255,.25);
  box-shadow: 0 2px 10px rgba(0,0,0,.6);
}
html.dark .resizable-tree-wrapper:hover {
  box-shadow: 0 6px 22px rgba(0,0,0,.72);
}
/* 细内阴影强调边界 */
.resizable-tree-wrapper::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.4);
  mix-blend-mode: overlay;
}
html.dark .resizable-tree-wrapper::before {
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.18);
}
.tree-header {
  padding-bottom: 8px;
  flex-shrink: 0;
}
.project-tree {
  flex: 1;
  overflow: auto;
  /* Custom scrollbar for better look */
  &::-webkit-scrollbar {
    width: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background: var(--el-border-color);
    border-radius: 3px;
  }
  &::-webkit-scrollbar-track {
    background: transparent;
  }
}
.node-row {
  display: flex;
  align-items: center;
  width: 100%;
  padding: 2px 4px;
  border-radius: 4px;
  transition: background .15s;
}
.node-row:hover {
  background: var(--el-color-primary-light-9);
}
.node-row.editing {
  background: var(--el-color-primary-light-8);
}
.node-left {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
}
.label {
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}
.folder-icon, .file-icon, .custom-icon {
  font-size: 16px;
  color: var(--el-text-color-secondary);
  flex-shrink: 0;
}
.node-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  opacity: 0;
  margin-left: auto;
  transition: opacity .15s;
  flex-shrink: 0;
}
.node-row:hover .node-actions {
  opacity: 1;
}
.rename-input {
  width: 140px;
}
.actions-trigger {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}
.more-icon {
  margin-left: 2px;
}
:deep(.danger-item) {
  color: var(--el-color-danger);
}
html.dark .node-row:hover {
  background: rgba(255,255,255,.08);
}
html.dark .node-row.editing {
  background: rgba(255,255,255,.12);
}
.component-placeholder, .panel-loading-overlay { display:none; }
</style>
